Go to Object Mode and run this code in the Scripting workspace:
from bpy import context as C
# you might want to disable modifiers before evaluating this depsgraph
dg = C.evaluated_depsgraph_get()
# deselect faces and edges
for fe in C.object.data.polygons[:] + C.object.data.edges[:]:
fe.select = False
for v in C.object.data.vertices:
co = v.co + v.normal * 0.0001
# https://docs.blender.org/api/current/bpy.types.Scene.html#bpy.types.Scene.ray_cast
ray_data = C.scene.ray_cast(dg, co, v.normal)
# unpack; underscored variables aren't used
result, _location, normal, _index, _object, _matrix = ray_data
# alternatives:
# result, _, normal, _, _, _ = ray_data
# or
# result = ray_data[0], normal = ray_data[2]
# positive dot product says the angle between directions is less than 180d
is_hit_from_inside = v.normal @ normal > 0
v.select = result and is_hit_from_inside
You can codegolf this code into a macro:
exec('d=C.evaluated_depsgraph_get()\nm=C.object.data\nfor i in m.polygons[:]+m.edges[:]:i.select=0\nfor v in m.vertices:v.select=(r:=C.scene.ray_cast(d,v.co+(n:=v.normal)*0.0001,n))[0]and n@r[2]>0');C.area.type='VIEW_3D'
[I used inline code formatting so it's easier to select]
You can now hit ⬆ ShiftF4, paste the macro (or type a macro hotstring or whatever), ↩ Enter and it will return to the viewport with the vertices selected. Again you need to be in Object Mode.
After opening the file, press the "Play" ▶ button, then enter edit mode and you should see some edges have gradients because not visible vertices are selected. Pressing ⎇ AltZ should reveal those vertices.
Same raycast principle could be used in geonodes: https://blender.stackexchange.com/a/302336/60486