The problem here is that makeindex can't deal with conflicting encaps (the location formatting command) for the same location. For example, page (location) 1 has an empty encap (from \index{Lahm}) and the encap nn (from \index{Lahm|nn}). Since makeindex doesn't know how to resolve this, it adds both 1 and \nn{1} to the location list for Lahm and issues a warning in the transcript. Assuming the file is called test.tex, then the transcript file is test.ilg and contains three such warnings:
Generating output file test.ind....
## Warning (input = test.idx, line = 4; output = test.ind, line = 3):
-- Conflicting entries: multiple encaps for the same page under same key.
## Warning (input = test.idx, line = 6; output = test.ind, line = 7):
-- Conflicting entries: multiple encaps for the same page under same key.
## Warning (input = test.idx, line = 2; output = test.ind, line = 11):
-- Conflicting entries: multiple encaps for the same page under same key.
done (13 lines written, 3 warnings).
There's no easy way to fix this with makeindex other than to process test.idx after the latex call and before the makeindex call using some form of script. (The makeglossaries perl script that comes with the glossaries package does this if it detects makeindex's multiple encap warning.)
The simplest solution is to switch to xindy, which resolves this conflict by giving preference to the formatting command (encap value) according to its position in the list of defined attributes. There are two attributes in your MWE: default (the default used when the | part is omitted from \index) and nn. These need to be defined in a xindy module (.xdy file). For example:
(define-attributes (( "default" "nn" )))
This gives the default attribute a higher priority. Alternatively:
(define-attributes (( "nn" "default" )))
This gives the nn attribute a higher priority. Unfortunately, texindy (the usual method of calling xindy when used with packages like makeidx) defines "default" in the latex-loc-fmts.xdy module, so both the above will cause the warning:
Loading module "latex-loc-fmts.xdy"...
WARNING: ignoring redefinition of attribute "default" in
(DEFINE-ATTRIBUTES (("default" "textbf" "textit" "hyperpage")))
However, if we make sure the custom .xdy module is loaded first, that will ensure the custom attribute list takes precedence.
There is another problem here. In the case of the B in your example, the \index{B|nn} entry will be discarded in favour of the \index{B} entry on page 1 when the default attribute overrides the nn attribute. This means that the location list for B will end up as 1, \nn{2}, \nn{3} (since \nn{1} has been discarded it can't form a range with \nn{2} and \nn{3}). This can be fixed with a merge rule:
(merge-to "default" "nn" :drop)
You also need to tell xindy what to do with the nn attribute:
(markup-locref :open "\nn{" :close "}" :attr "nn")
This just encapsulates the location with \nn (which is what makeindex does automatically). The default attribute doesn't need any special formatting.
Finally, the location list separators need to be defined. For example, to approximately replicate makeindex:
(markup-locref-list :sep ", ")
(markup-range :sep "--")
However, there's a difference between makeindex and xindy ranges where there is an encap value. With makeindex, your MWE encapsulates the range (for example, \nn{1--3}) whereas xindy encapsulates the range elements instead (for example, \nn{1}--\nn{3}). Similarly for consecutive encaps (for example, \nn{1, 2} with makeindex vs \nn{1}, \nn{2} for xindy). This means that the range will end up appearing as 1n--3n with xindy rather than 1--3n. Since the \nn command now just encapsulates a single location, the definition is much simpler:
\newcommand{\nn}[1]{#1n}
but the range formation needs adjusting to prevent the n prefix from being applied to the starting location.
One way to get around it is to modify the range separator so that instead of simply separating ranges with -- it uses a formatting command \range{start}{end}:
(markup-range :open "\range{" :sep "}{" :close "}")
This will change \nn{1}--\nn{3} to \range{\nn{1}}{\nn{3}}. Now \range can be defined to locally alter \nn within the scope of the start value:
\newcommand{\range}[2]{{\def\nn##1{##1}#1}--#2}
Here's the complete MWE:
\documentclass{scrartcl}
\usepackage{filecontents}
\usepackage[T1]{fontenc}
\usepackage[ngerman]{babel}
\usepackage{makeidx}
\begin{filecontents*}{\jobname.xdy}
; list of allowed attributes
(define-attributes (( "default" "nn" )))
; define format to use for locations
(markup-locref :open "\nn{" :close "}" :attr "nn")
; location list separators
(markup-locref-list :sep ", ")
(markup-range :open "\range{" :sep "}{" :close "}")
(merge-to "default" "nn" :drop)
\end{filecontents*}
\makeindex
\newcommand{\nn}[1]{#1n}
\newcommand{\range}[2]{{\def\nn##1{##1}#1}--#2}
\begin{document}
bla\index{Lahm} blablab\index{Lahm|nn}
bla\index{A} bla\index{A|nn}
bla\index{B} bla\index{B|nn}
\newpage
bla\index{Lahm|nn} blablab
bla\index{B|nn}
\newpage
bla\index{B|nn}
\printindex
\end{document}
Assuming this file is called test.tex then the build process is:
pdflatex test
xindy -M test -M texindy -C utf8 -L english -t test.ilg test.idx
pdflatex test
Alternatively (since you're using ngerman):
pdflatex test
xindy -M test -M texindy -C din5007-utf8 -L german -t test.ilg test.idx
pdflatex test
Note that the order of the -M switches is important. This ensures that the attribute list in test.xdy comes before the attribute list in the texindy modules. (However, this only matters if you need to change the precedence of the attributes defined by the texindy modules.)

Index
A
A, 1
B
B, 1–3n
L
Lahm, 1, 2n
You can remove the letter group headings by adding the following to your document:
\newcommand*\lettergroup[1]{}
If you need any more encaps, you can just add them to the list of defined attributes, so to add ii from your comment:
(define-attributes (( "default" "nn" "ii")))
(This makes the default and nn encaps override ii.) You don't even need to define \ii (although you still can if you like). For example:
(markup-locref :open "\emph{" :close "}" :attr "ii")
(which just uses \emph directly) or
(markup-locref :open "\ii{" :close "}" :attr "ii")
(which uses \ii).
makeindex. Are you happy to tryxindyinstead? – Nicola Talbot Apr 05 '17 at 14:04