This is a fully coded Python solution based on the direction provided by @eldesgraciado.
This code assumes that you are already working with the properly binarized white-on-black image (e.g. after grayscale conversion, black hat morphing and Otsu's thesholding) - OpenCV documentation recommends working with the binarized images with the white foreground when applying morphological operations and stuff like that.
num_comps, labeled_pixels, comp_stats, comp_centroids = \
cv2.connectedComponentsWithStats(thresh_image, connectivity=4)
min_comp_area = 10 # pixels
# get the indices/labels of the remaining components based on the area stat
# (skip the background component at index 0)
remaining_comp_labels = [i for i in range(1, num_comps) if comp_stats[i][4] >= min_comp_area]
# filter the labeled pixels based on the remaining labels,
# assign pixel intensity to 255 (uint8) for the remaining pixels
clean_img = np.where(np.isin(labeled_pixels,remaining_comp_labels)==True,255,0).astype('uint8')
The advantage of this solution is that it allows you to filter out the noise without negatively affecting the characters that may already be compromised.
I work with dirty scans that have the undesirable effects like merged characters and character erosion, and I found out the hard way that there is no free lunch - even a seemingly harmless opening operation with the 3x3 kernel and one iteration results in some character degradation (despite being very effective for removing the noise around the characters).
So if the character quality allows, blunt cleanup operations on the entire image (e.g. blurring, opening, closing) are OK, but if not - this should be done first.
P.S. One more thing - you should not be using a lossy format like JPEG when working with text images, use a lossless format like PNG instead.