This looks ok to me.  I haven't seen pointers like this very much in practice, but I would describe tags as "A pointer to a 2-size array of character pointers".  When you malloc (2 * sizeof *tags);, you create space for two of of these 2-size array of character pointers.  This is what you have from each line:
char *(*tags)[2] = malloc (2 * sizeof *tags);
tags -----> [char*][char*]   // index 0 of tags
            [char*][char*]   // index 1 of tags
// You now have 4 character pointers in contiguous memory that don't
// point to anything. tags points to index 0, tags+1 points to index 1.
// Each +1 on *(tags) advances the pointer 16 bytes on my machine (the size
// of two 8-byte pointers).
Next malloc:
tags[0][0] = malloc(sizeof(char)*5);
               +-------> 5 bytes
               |
tags ------> [char*][char*]  // index 0
               ^
               index 0 of tags[0], this now points to 5 bytes
After first strcpy
strcpy(tags[0][0],"hi");
               +-------> {'h', 'i', '\0', <2 more> }
               |
tags ------> [char*][char*]  // index 0
Next malloc
tags[0][1] = malloc(sizeof(char)*5);
                      +-------> 5 bytes
                      |
tags ------> [char*][char*]  // index 0
                      ^
                      index 1 of tags[0], this now points to 5 bytes
Next strcpy
strcpy(tags[0][1],"h2");
                      +-------> {'h', '2', '\0', <2 more> }
                      |
tags ------> [char*][char*]  // index 0
And finally, the string literal assignments
tags[1][0] = "<head";
tags[1][1] = "</head>";
tags -----> [char*][char*]   // index 0 of tags
            [char*][char*]   // index 1 of tags
              |       |
              |       |------> points to string literal "</head>"
              |--------------> points to string literal "<head"
If you really want to clean up properly, you should
free(tags[0][1]);
free(tags[0][0]);
// the order of the above doesn't really matter, I just get in the
// habit of cleaning up in the reverse order that I malloced in.
// But if you free(tags) first, you've created a memory leak, as
// there's now no existing pointers to tags[0][0] or [0][1].
free(tags);
Of course, all memory gets reclaimed by the OS as soon as the process exits anyway.