We can do this with geometry nodes, with a few tricks involved. We'll start with an no-subdiv icosphere like you have. We have no custom normals on this (we'll get to those in a bit.) We'll start with face normals.

There's actually more than one kind of face normal in Blender. Because quads and ngons can be non-planar, they can have their own face normals that are different than their triangulated normals. Here, I'm assuming you want the triangulated face normals, and we're starting by triangulating the mesh (even though in the case of our icosphere, it's already triangulated.) The nodes are pretty simple; we just turn our faces to points, align with the nearest normal, and instance our arrow mesh. Testing it out, the domain of "face" normals seems to be set by choosing "faces" in the mesh-to-points node.
We'll group that and move on to vertex normals. It wouldn't make any sense to show vertex normals in the middle of our faces; the interpolated vertex normal there is (right now) the same as the face normal. So instead, we'll instance these onto the vertices:

You can see this is roughly the same structure, except we're instancing on our vertices, making it point domain data.
Custom normals are where we have to get tricky. First, of course, we're going to need an arrow for every face-corner; additionally, there's little GN support for direct use of custom normals, so we'll have to hack it.
Rather than instancing on our previous object, I'm going to make a linked duplicate, autosmooth it, and then start from scratch with modifiers:

So what are we doing here?
Edge splitting all edges creates a unique vertex for every face-corner, which can receive its own custom normal and can spawn a new instance;
Our data transfer modifier creates the face-corner custom normals (it can target a hidden copy of the mesh if you'd like). Here, I'm copying normals from the wireframe Suzanne;
We make an attribute to back up our current position;
We do a constant displacement in the direction of custom normals.
At this point, we can find the custom normal of the vertex very easily, by simply subtracting its position from its old position (it's a unit-length displacement). We can also restore our original position very easily.

Shown with other object at the same time. We're figuring out our custom normal by subtract posBack from position rather than using a Normal node. Note that we're transforming our instances by -1 in their Z axis to counteract our displacement. Otherwise, this is the same thing as our vertex normals. We're not joining to our original geometry, just outputting the arrows, because we intend to use this object in conjunction with our other object, and because our original geometry is no longer much like the icosphere with which we started :)
Finally, we can enable cage display on all modifiers, parent both the source of our custom normals and our custom normal display to our object that displays face and vertex normals, and we can make any edits to the original object (or, to our source of normals) to see them displayed, in real time:


In your question's image, you're using 3 arrows for each normal. We could easily use a set of 3 arrows instead of a single one for each instance. However, what we can't do, is align those with a UV tangent vector. So we can't use this to, for example, demonstrate tangent space. It would be nice if there was some way to get UV tangents into geometry nodes, but that's life.