If you would like to apply the same kernels on add_features, you could push the add_features dimension to the batch dim and apply the standard nn.Conv2d.
Here is a small dummy example with the comparison of the manual approach:
batch_size, add_features, c, h, w = 2, 4, 3, 24, 24

x = torch.randn(batch_size, add_features, c, h, w)
x_ = x.view(batch_size*add_features, c, h, w) # push add_features into batch dim

conv = nn.Conv2d(c, 1, 3, 1, 1, bias=False)
output = conv(x_)
output = output.view(batch_size, add_features, 1, h, w)

# manual approach to verify output
outputs = []
for idx in range(add_features):
    x_part = x[:, idx]
    # get kernel
    kernel = conv.weight
    out = F.conv2d(x_part, kernel, padding=1)
    outputs.append(out)

outputs = torch.stack(outputs)
outputs = outputs.permute(1, 0, 2, 3, 4)

print((outputs - output).abs().max())
> tensor(0., grad_fn=<MaxBackward1>)

Let me know, if that would work for you.